Skip to content

ci(e2e): run the Playwright suite on pull requests, sharded four ways - #494

Draft
DavidBabinec wants to merge 2 commits into
mainfrom
ci/e2e-workflow
Draft

ci(e2e): run the Playwright suite on pull requests, sharded four ways#494
DavidBabinec wants to merge 2 commits into
mainfrom
ci/e2e-workflow

Conversation

@DavidBabinec

@DavidBabinec DavidBabinec commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What changed

Runs the Playwright suite on every pull request, sharded four ways, against the built admin SPA served by the Bun server on one origin.

  • .github/workflows/e2e.yml: four-shard matrix, fail-fast: false, report artifacts on failure.
  • scripts/e2e-server.ts replaces scripts/e2e-dev.ts (bun run e2e:serve): wipe .tmp/e2e-*, bun run build, then serve dist/ and the public site from server/index.ts on 127.0.0.1:3002. No Vite dev server, no second port, no dev-only proxy plugins in the loop.
  • playwright.config.ts: one base URL for admin and public; the webServer budget covers the build; forbidOnly under CI.
  • tests/e2e/helpers/constants.ts exports ADMIN_BASE_URL; account.e2e.ts imports it instead of duplicating the literal.
  • tests/e2e/helpers/editor.ts gains deleteTemplate. Every spec that publishes a Posts template now removes it before it ends (five sites across content-seo-meta, content, visual-builder).
  • docs/e2e/README.md documents the stack and the new Template rule; the devWorkflow gate test asserts the new shape.

Why

The first Actions run failed on all four shards identically: the CMS came up, Playwright waited on the Vite dev server at :5174, and Vite printed nothing for two minutes before taking SIGTERM still alive. The Vite dev server hangs on Linux runners under Bun. A cold start here is 267 ms, so nothing local could catch it. vite build is a batch step and already passes on Linux CI on every PR; only the long-lived dev server is the problem, so the suite stops depending on it. It now exercises the bundle users actually get.

One origin is safe by design: the session cookie is Path=/admin (server/handlers/cms/session.ts) and visitPublicPage opens a fresh browser context, so anonymity never depended on the port. Public specs were already hitting Bun on :3002; only the admin moves, from Vite-served to dist-served.

Running the suite in one process for the first time also surfaced a shared-database leak that sharding had hidden. content-seo-meta published a Posts template at priority 300 and never removed it, and the template chain keeps one template per breadth level (highest priority, then document order), so CONTENT-008 (priority 150) and SITE-018 (a 300 that loses the tie) rendered through the wrong template whenever it ran first. Fixed at the source with cleanup, not by quarantining.

Impact

  • CI: E2E becomes a real gate. Not added to branch protection in this PR; watch it for a few PRs first.
  • Developers: bunx playwright test now builds first (about 12 s locally). E2E_REUSE_SERVER=1 plus E2E_ADMIN_BASE_URL still points the suite at a server you started yourself.
  • Not fixed here, tracked separately: CONTENT-009 fails in a full single-process run because createUntitledEntry picks the new slug from the client-side entries.length, and an earlier spec leaves an unsaved untitled row behind. That is a product bug (a real user with a slow list load gets "Could not create entry"). It passes under sharding, which is why CI is green and the bug is still real.

Verification

Local, Chromium, new harness, real exit codes:

Run Result
--project=setup 1 passed
ordered content-seo-meta, content, visual-builder (the leak ordering) 38 passed
--shard=1/4 68 passed
--shard=2/4 29 passed (1 failed before the template cleanup)
--shard=3/4 37 passed
--shard=4/4 44 passed
full suite, one process 161 passed, 1 failed (CONTENT-009, see above), 1 flaky (SITE-005 drag, passes on retry); was 159 / 3 / 1 before the cleanup
bunx eslint tests/e2e/… and bunx tsc -p tests/e2e/tsconfig.json --noEmit clean

The suite was excluded from CI because a serial run took most of an hour and
timed out unpredictably. Nothing has gated it since, which is how three specs
rotted on main behind security fixes nobody saw break them (#492).

Each shard boots its own e2e:dev stack on its own disposable SQLite database,
so the shards share no state and the setup projects run once per shard.
Measured locally with CI=1: shard 1 of 4 is 68 tests in 6 minutes, against
15.6 minutes for the whole suite serially.

It runs as its own workflow so a browser run never delays the build, lint and
test feedback a PR needs first. forbidOnly is on under CI, so a stray .only
can no longer green the suite by running a single test. Reports and traces
upload as artifacts when a shard fails.
The first Actions run failed on every shard the same way: the Bun CMS
came up, Playwright waited on the Vite dev server at :5174, and Vite
printed nothing for two minutes before taking SIGTERM still alive. The
dev server hangs on Linux runners under Bun. A cold start here is 267 ms,
so nothing local could have caught it.

Rather than nurse the dev server through CI, the E2E stack now builds
the admin SPA and serves it from the same Bun process that serves the
published site. No Vite dev server, no second port, no dev-only proxy
plugins in the loop. `vite build` is a batch step and already passes on
Linux CI on every PR; it was only the long-lived dev server that did not.
The suite now exercises the bundle users actually get.

One origin is safe by design: the session cookie is `Path=/admin`, and
`visitPublicPage` opens a fresh browser context, so anonymity never
depended on the port. The public specs were already hitting Bun on :3002
directly; only the admin moves, from Vite-served to dist-served.

`scripts/e2e-dev.ts` is replaced by `scripts/e2e-server.ts`
(`bun run e2e:serve`), the Playwright `webServer` budget covers the
build, `tests/e2e/helpers/constants.ts` exports one `ADMIN_BASE_URL`
that `account.e2e.ts` now imports instead of duplicating, and the
`devWorkflow` gate test asserts the new shape.

Running the whole suite in one process for the first time also surfaced
a shared-state leak that sharding had hidden: `content-seo-meta` publishes
a Posts template at priority 300 and never removes it, and the template
chain keeps one template per breadth level, so CONTENT-008 (priority 150)
and SITE-018 (a 300 that loses the tie on document order) rendered through
the wrong template whenever that spec ran first. A shared `deleteTemplate`
helper now removes every Posts template a spec publishes before the spec
ends, in all five places one is created.

Verification (local, Chromium, new harness):
  bunx playwright test --project=setup            1 passed
  content-seo-meta + content + visual-builder     38 passed (the leak ordering)
  bunx playwright test --shard=1/4                68 passed
  bunx playwright test --shard=2/4                29 passed (was 1 failed before the cleanup)
  bunx playwright test --shard=3/4                37 passed
  bunx playwright test --shard=4/4                44 passed
  full suite, one process                         161 passed, 1 failed, 1 flaky
    failed: CONTENT-009, the client-side untitled-slug pick (product bug,
            tracked separately); flaky: SITE-005 drag (timing, passes on retry)
  bunx eslint tests/e2e/…                         clean
  bunx tsc -p tests/e2e/tsconfig.json --noEmit    clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant